@rubytech/create-realagent 1.0.853 → 1.0.855
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +9 -3
- package/package.json +1 -1
- package/payload/platform/lib/persistent-components/dist/index.d.ts +21 -0
- package/payload/platform/lib/persistent-components/dist/index.d.ts.map +1 -0
- package/payload/platform/lib/persistent-components/dist/index.js +32 -0
- package/payload/platform/lib/persistent-components/dist/index.js.map +1 -0
- package/payload/platform/lib/persistent-components/src/index.ts +28 -0
- package/payload/platform/lib/persistent-components/tsconfig.json +8 -0
- package/payload/platform/package.json +2 -2
- package/payload/platform/plugins/admin/PLUGIN.md +1 -1
- package/payload/platform/plugins/admin/hooks/__tests__/playwright-file-guard.test.sh +278 -0
- package/payload/platform/plugins/admin/hooks/playwright-file-guard.sh +204 -20
- package/payload/platform/plugins/admin/mcp/dist/index.js +40 -1
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/cloudflare/references/manual-setup.md +2 -0
- package/payload/platform/plugins/cloudflare/scripts/list-cf-domains.sh +39 -10
- package/payload/platform/plugins/cloudflare/scripts/list-cf-domains.ts +112 -20
- package/payload/platform/plugins/docs/references/cloudflare.md +1 -0
- package/payload/platform/plugins/docs/references/deployment.md +2 -0
- package/payload/platform/plugins/docs/references/getting-started.md +2 -0
- package/payload/platform/plugins/docs/references/platform.md +1 -1
- package/payload/platform/plugins/docs/references/troubleshooting.md +10 -0
- package/payload/platform/scripts/admin-persist-audit.ts +191 -0
- package/payload/platform/scripts/component-knowledgedoc-backfill.ts +214 -0
- package/payload/platform/scripts/installer-device-verify.sh +17 -4
- package/payload/platform/templates/specialists/agents/content-producer.md +2 -2
- package/payload/server/chunk-CFNSKDGA.js +667 -0
- package/payload/server/chunk-DC6DWYZJ.js +1603 -0
- package/payload/server/chunk-LTB5SSQW.js +10889 -0
- package/payload/server/chunk-MN2LGNUB.js +2143 -0
- package/payload/server/client-pool-AMT2W3II.js +34 -0
- package/payload/server/cloudflare-task-tracker-LJ4SMK2D.js +20 -0
- package/payload/server/maxy-edge.js +3 -3
- package/payload/server/public/assets/admin-Cpk5cT4I.js +352 -0
- package/payload/server/public/assets/public-DApUXgoq.js +5 -0
- package/payload/server/public/assets/useVoiceRecorder-CI8GpxfU.js +36 -0
- package/payload/server/public/index.html +2 -2
- package/payload/server/public/public.html +2 -2
- package/payload/server/server.js +543 -354
- package/payload/server/public/assets/admin-Dyl8uNxX.js +0 -352
- package/payload/server/public/assets/public-B_PNZUph.js +0 -5
- package/payload/server/public/assets/useVoiceRecorder-fD0IWzJj.js +0 -36
package/dist/index.js
CHANGED
|
@@ -2450,9 +2450,15 @@ function installService() {
|
|
|
2450
2450
|
// Mirrors paths.ts and vnc.sh so all four sites compute the same numbers
|
|
2451
2451
|
// regardless of which one reads brand.json first.
|
|
2452
2452
|
const VNC_OFFSET = VNC_DISPLAY - 99;
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2453
|
+
// Parenthesise the deterministic derivation. Operator-precedence already
|
|
2454
|
+
// groups (offset + base) ahead of nullish-coalesce, so this is purely a
|
|
2455
|
+
// textual change for readability and to satisfy Task 954's grep audit
|
|
2456
|
+
// (the runtime path was the real silent default; these are install-time
|
|
2457
|
+
// deterministic fallbacks but the audit is a single regex over the
|
|
2458
|
+
// codebase).
|
|
2459
|
+
const RFB_PORT = BRAND.rfbPort ?? (5900 + VNC_OFFSET);
|
|
2460
|
+
const WEBSOCKIFY_PORT_BRAND = BRAND.websockifyPort ?? (6080 + VNC_OFFSET);
|
|
2461
|
+
const CDP_PORT_BRAND = BRAND.cdpPort ?? (9222 + VNC_OFFSET);
|
|
2456
2462
|
// Task 924/938 pre-flight — refuse to write service files if any of the
|
|
2457
2463
|
// three brand-scoped ports is already held by a process that is NOT this
|
|
2458
2464
|
// brand's own on-demand browser nor a peer brand's edge stack.
|
package/package.json
CHANGED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The component names that the platform UI treats as long-lived editor
|
|
3
|
+
* surfaces (not single-shot prompts). They survive multiple `onSubmit`
|
|
4
|
+
* fires because the operator may auto-save mid-edit, and they are also
|
|
5
|
+
* the **server-side commitment surface** for Task 942: when the admin
|
|
6
|
+
* agent emits one of these names via `render-component`, the live
|
|
7
|
+
* stream-parser materialises the inline content as a sibling
|
|
8
|
+
* `:KnowledgeDocument` artefact so the row appears in the artefacts
|
|
9
|
+
* panel and survives session compaction.
|
|
10
|
+
*
|
|
11
|
+
* Single source of truth — the platform UI (`app/admin-types.ts`) and
|
|
12
|
+
* the admin MCP server (`plugins/admin/mcp/src/index.ts`) both import
|
|
13
|
+
* from this module. Any drift here is an account-isolation /
|
|
14
|
+
* persistence-doctrine bug; keep the constant in one place.
|
|
15
|
+
*/
|
|
16
|
+
export declare const PERSISTENT_COMPONENTS: Set<string>;
|
|
17
|
+
/**
|
|
18
|
+
* Cheap, allocation-free guard for hot-path checks.
|
|
19
|
+
*/
|
|
20
|
+
export declare function isPersistentComponent(name: string | undefined | null): boolean;
|
|
21
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,qBAAqB,aAKhC,CAAC;AAEH;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,OAAO,CAE9E"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PERSISTENT_COMPONENTS = void 0;
|
|
4
|
+
exports.isPersistentComponent = isPersistentComponent;
|
|
5
|
+
/**
|
|
6
|
+
* The component names that the platform UI treats as long-lived editor
|
|
7
|
+
* surfaces (not single-shot prompts). They survive multiple `onSubmit`
|
|
8
|
+
* fires because the operator may auto-save mid-edit, and they are also
|
|
9
|
+
* the **server-side commitment surface** for Task 942: when the admin
|
|
10
|
+
* agent emits one of these names via `render-component`, the live
|
|
11
|
+
* stream-parser materialises the inline content as a sibling
|
|
12
|
+
* `:KnowledgeDocument` artefact so the row appears in the artefacts
|
|
13
|
+
* panel and survives session compaction.
|
|
14
|
+
*
|
|
15
|
+
* Single source of truth — the platform UI (`app/admin-types.ts`) and
|
|
16
|
+
* the admin MCP server (`plugins/admin/mcp/src/index.ts`) both import
|
|
17
|
+
* from this module. Any drift here is an account-isolation /
|
|
18
|
+
* persistence-doctrine bug; keep the constant in one place.
|
|
19
|
+
*/
|
|
20
|
+
exports.PERSISTENT_COMPONENTS = new Set([
|
|
21
|
+
'action-list',
|
|
22
|
+
'document-editor',
|
|
23
|
+
'rich-content-editor',
|
|
24
|
+
'grid-editor',
|
|
25
|
+
]);
|
|
26
|
+
/**
|
|
27
|
+
* Cheap, allocation-free guard for hot-path checks.
|
|
28
|
+
*/
|
|
29
|
+
function isPersistentComponent(name) {
|
|
30
|
+
return typeof name === 'string' && exports.PERSISTENT_COMPONENTS.has(name);
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAyBA,sDAEC;AA3BD;;;;;;;;;;;;;;GAcG;AACU,QAAA,qBAAqB,GAAG,IAAI,GAAG,CAAS;IACnD,aAAa;IACb,iBAAiB;IACjB,qBAAqB;IACrB,aAAa;CACd,CAAC,CAAC;AAEH;;GAEG;AACH,SAAgB,qBAAqB,CAAC,IAA+B;IACnE,OAAO,OAAO,IAAI,KAAK,QAAQ,IAAI,6BAAqB,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;AACrE,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The component names that the platform UI treats as long-lived editor
|
|
3
|
+
* surfaces (not single-shot prompts). They survive multiple `onSubmit`
|
|
4
|
+
* fires because the operator may auto-save mid-edit, and they are also
|
|
5
|
+
* the **server-side commitment surface** for Task 942: when the admin
|
|
6
|
+
* agent emits one of these names via `render-component`, the live
|
|
7
|
+
* stream-parser materialises the inline content as a sibling
|
|
8
|
+
* `:KnowledgeDocument` artefact so the row appears in the artefacts
|
|
9
|
+
* panel and survives session compaction.
|
|
10
|
+
*
|
|
11
|
+
* Single source of truth — the platform UI (`app/admin-types.ts`) and
|
|
12
|
+
* the admin MCP server (`plugins/admin/mcp/src/index.ts`) both import
|
|
13
|
+
* from this module. Any drift here is an account-isolation /
|
|
14
|
+
* persistence-doctrine bug; keep the constant in one place.
|
|
15
|
+
*/
|
|
16
|
+
export const PERSISTENT_COMPONENTS = new Set<string>([
|
|
17
|
+
'action-list',
|
|
18
|
+
'document-editor',
|
|
19
|
+
'rich-content-editor',
|
|
20
|
+
'grid-editor',
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Cheap, allocation-free guard for hot-path checks.
|
|
25
|
+
*/
|
|
26
|
+
export function isPersistentComponent(name: string | undefined | null): boolean {
|
|
27
|
+
return typeof name === 'string' && PERSISTENT_COMPONENTS.has(name);
|
|
28
|
+
}
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
"plugins/*/mcp"
|
|
7
7
|
],
|
|
8
8
|
"scripts": {
|
|
9
|
-
"build": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/oauth-llm/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/account-enumeration/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json && tsc -p lib/brand-templating/tsconfig.json && tsc -p lib/entitlement/tsconfig.json && tsc -p lib/task-secrets/tsconfig.json && tsc -p lib/admins-write/tsconfig.json && NODE_OPTIONS='--max-old-space-size=8192' tsc -b plugins/*/mcp/tsconfig.json",
|
|
10
|
-
"build:lib": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/oauth-llm/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/account-enumeration/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json && tsc -p lib/brand-templating/tsconfig.json && tsc -p lib/entitlement/tsconfig.json && tsc -p lib/task-secrets/tsconfig.json && tsc -p lib/admins-write/tsconfig.json",
|
|
9
|
+
"build": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/oauth-llm/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/account-enumeration/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json && tsc -p lib/brand-templating/tsconfig.json && tsc -p lib/entitlement/tsconfig.json && tsc -p lib/task-secrets/tsconfig.json && tsc -p lib/admins-write/tsconfig.json && tsc -p lib/persistent-components/tsconfig.json && NODE_OPTIONS='--max-old-space-size=8192' tsc -b plugins/*/mcp/tsconfig.json",
|
|
10
|
+
"build:lib": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/oauth-llm/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/account-enumeration/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json && tsc -p lib/brand-templating/tsconfig.json && tsc -p lib/entitlement/tsconfig.json && tsc -p lib/task-secrets/tsconfig.json && tsc -p lib/admins-write/tsconfig.json && tsc -p lib/persistent-components/tsconfig.json",
|
|
11
11
|
"build:memory": "tsc -p plugins/memory/mcp/tsconfig.json",
|
|
12
12
|
"build:contacts": "tsc -p plugins/contacts/mcp/tsconfig.json",
|
|
13
13
|
"build:telegram": "tsc -p plugins/telegram/mcp/tsconfig.json",
|
|
@@ -71,5 +71,5 @@ Tools are available via the `admin` MCP server.
|
|
|
71
71
|
## Hooks
|
|
72
72
|
|
|
73
73
|
- `hooks/pre-tool-use.sh` — enforces admin agent write boundaries
|
|
74
|
-
- `hooks/playwright-file-guard.sh` —
|
|
74
|
+
- `hooks/playwright-file-guard.sh` — rewrites file:// URLs to a backgrounded loopback http.server before Playwright sees them
|
|
75
75
|
- `hooks/webfetch-preflight.mjs` — short-circuits WebFetch on JS-SPA shells with a structured `WEBFETCH_CANNOT_READ_JS_SPA` error so the agent surfaces a loud failure to the owner instead of paying the 60s extraction timeout. Fail-open on any internal error.
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Regression test for playwright-file-guard.sh (Task 944).
|
|
3
|
+
#
|
|
4
|
+
# Covers:
|
|
5
|
+
# 1. file:// URL → hook emits hookSpecificOutput.updatedInput.url JSON,
|
|
6
|
+
# starts a backgrounded python3 -m http.server on a free port rooted
|
|
7
|
+
# at the file's parent dir, writes /tmp/playwright-file-guard.<port>.pid,
|
|
8
|
+
# and the rewritten URL responds 200.
|
|
9
|
+
# 2. https:// URL → silent passthrough, exit 0, no stdout JSON.
|
|
10
|
+
# 3. Free-port under contention: hold 8080, run hook on file://, assert
|
|
11
|
+
# chosen port != 8080.
|
|
12
|
+
# 4. Stale-PID cleanup mode: pre-create stale PID file pointing at a real
|
|
13
|
+
# http.server we control, run `cleanup`, assert process killed + file
|
|
14
|
+
# removed.
|
|
15
|
+
# 5. Reused-PID guard: PID file points at non-http.server process,
|
|
16
|
+
# cleanup leaves the process alone.
|
|
17
|
+
# 6. Fail-open on missing python3 (PATH override).
|
|
18
|
+
# 7. Malformed stdin → exit 0 silent passthrough.
|
|
19
|
+
# 8. Terminal stdin guard preserved.
|
|
20
|
+
# 9. Hook source contains no copy of the old menu phrase.
|
|
21
|
+
|
|
22
|
+
set -u
|
|
23
|
+
|
|
24
|
+
HOOK="$(cd "$(dirname "$0")/.." && pwd)/playwright-file-guard.sh"
|
|
25
|
+
if [[ ! -x "$HOOK" ]]; then
|
|
26
|
+
echo "FAIL: $HOOK not executable" >&2
|
|
27
|
+
exit 1
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
# Track every PID we spawn so we can reap on EXIT.
|
|
31
|
+
TRACKED_PIDS=()
|
|
32
|
+
TMPFILES=()
|
|
33
|
+
|
|
34
|
+
cleanup_test_state() {
|
|
35
|
+
for p in "${TRACKED_PIDS[@]:-}"; do
|
|
36
|
+
[[ -n "$p" ]] && kill -9 "$p" 2>/dev/null || true
|
|
37
|
+
done
|
|
38
|
+
for f in "${TMPFILES[@]:-}"; do
|
|
39
|
+
[[ -n "$f" ]] && rm -f "$f" 2>/dev/null || true
|
|
40
|
+
done
|
|
41
|
+
rm -f /tmp/playwright-file-guard.*.pid 2>/dev/null || true
|
|
42
|
+
}
|
|
43
|
+
trap cleanup_test_state EXIT
|
|
44
|
+
|
|
45
|
+
PASS=0
|
|
46
|
+
FAIL=0
|
|
47
|
+
|
|
48
|
+
pass() { echo "PASS: $1"; PASS=$((PASS + 1)); }
|
|
49
|
+
fail() { echo "FAIL: $1" >&2; FAIL=$((FAIL + 1)); }
|
|
50
|
+
|
|
51
|
+
# Sweep stale PID files BEFORE running tests so prior runs don't poison the
|
|
52
|
+
# fixture. The hook's opportunistic cleanup also triggers on every call, but
|
|
53
|
+
# a clean slate makes assertions deterministic.
|
|
54
|
+
rm -f /tmp/playwright-file-guard.*.pid 2>/dev/null || true
|
|
55
|
+
|
|
56
|
+
# ---- Test 1: file:// rewrite happy path -------------------------------------
|
|
57
|
+
|
|
58
|
+
TEST_HTML=$(mktemp -t playwright-file-guard-XXXXXX).html
|
|
59
|
+
echo "<html><body>hello</body></html>" > "$TEST_HTML"
|
|
60
|
+
TMPFILES+=("$TEST_HTML")
|
|
61
|
+
TEST_BASENAME=$(basename "$TEST_HTML")
|
|
62
|
+
|
|
63
|
+
INPUT_JSON=$(printf '{"hook_event_name":"PreToolUse","tool_name":"mcp__plugin_playwright_playwright__browser_navigate","tool_input":{"url":"file://%s"}}' "$TEST_HTML")
|
|
64
|
+
STDOUT_FILE=$(mktemp); STDERR_FILE=$(mktemp); TMPFILES+=("$STDOUT_FILE" "$STDERR_FILE")
|
|
65
|
+
|
|
66
|
+
printf '%s' "$INPUT_JSON" | bash "$HOOK" >"$STDOUT_FILE" 2>"$STDERR_FILE"
|
|
67
|
+
RC=$?
|
|
68
|
+
|
|
69
|
+
if [[ "$RC" -ne 0 ]]; then
|
|
70
|
+
fail "Test 1: hook exited $RC on file:// rewrite (expected 0)"
|
|
71
|
+
elif ! grep -q '"hookSpecificOutput"' "$STDOUT_FILE"; then
|
|
72
|
+
fail "Test 1: stdout missing hookSpecificOutput JSON. Got: $(cat "$STDOUT_FILE")"
|
|
73
|
+
elif ! grep -q '"updatedInput"' "$STDOUT_FILE"; then
|
|
74
|
+
fail "Test 1: stdout missing updatedInput field. Got: $(cat "$STDOUT_FILE")"
|
|
75
|
+
elif ! grep -qE '"url":\s*"http://127.0.0.1:[0-9]+/'"$TEST_BASENAME"'"' "$STDOUT_FILE"; then
|
|
76
|
+
fail "Test 1: stdout url not rewritten as expected. Got: $(cat "$STDOUT_FILE")"
|
|
77
|
+
elif ! grep -q '\[playwright-file-guard\] action=rewrite' "$STDERR_FILE"; then
|
|
78
|
+
fail "Test 1: stderr missing rewrite log line. Got: $(cat "$STDERR_FILE")"
|
|
79
|
+
else
|
|
80
|
+
PORT=$(grep -oE 'http://127\.0\.0\.1:[0-9]+/' "$STDOUT_FILE" | head -1 | grep -oE ':[0-9]+/' | tr -d ':/')
|
|
81
|
+
PID_FILE="/tmp/playwright-file-guard.${PORT}.pid"
|
|
82
|
+
if [[ ! -f "$PID_FILE" ]]; then
|
|
83
|
+
fail "Test 1: PID file $PID_FILE not created"
|
|
84
|
+
else
|
|
85
|
+
SERVER_PID=$(cat "$PID_FILE")
|
|
86
|
+
TRACKED_PIDS+=("$SERVER_PID")
|
|
87
|
+
HTTP_BODY=$(curl -s --max-time 2 "http://127.0.0.1:${PORT}/${TEST_BASENAME}" 2>/dev/null || true)
|
|
88
|
+
if echo "$HTTP_BODY" | grep -q "hello"; then
|
|
89
|
+
pass "Test 1: file:// → rewrite + spawned server responds 200"
|
|
90
|
+
else
|
|
91
|
+
fail "Test 1: spawned server at port $PORT did not return file content. Body: $HTTP_BODY"
|
|
92
|
+
fi
|
|
93
|
+
fi
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
# ---- Test 2: https:// passthrough -------------------------------------------
|
|
97
|
+
|
|
98
|
+
INPUT_JSON='{"hook_event_name":"PreToolUse","tool_name":"mcp__plugin_playwright_playwright__browser_navigate","tool_input":{"url":"https://example.com"}}'
|
|
99
|
+
STDOUT_FILE=$(mktemp); STDERR_FILE=$(mktemp); TMPFILES+=("$STDOUT_FILE" "$STDERR_FILE")
|
|
100
|
+
printf '%s' "$INPUT_JSON" | bash "$HOOK" >"$STDOUT_FILE" 2>"$STDERR_FILE"
|
|
101
|
+
RC=$?
|
|
102
|
+
|
|
103
|
+
if [[ "$RC" -ne 0 ]]; then
|
|
104
|
+
fail "Test 2: hook exited $RC on https:// passthrough"
|
|
105
|
+
elif [[ -s "$STDOUT_FILE" ]]; then
|
|
106
|
+
fail "Test 2: stdout should be empty on passthrough. Got: $(cat "$STDOUT_FILE")"
|
|
107
|
+
elif ! grep -q '\[playwright-file-guard\] action=passthrough scheme=https' "$STDERR_FILE"; then
|
|
108
|
+
fail "Test 2: stderr missing passthrough log line. Got: $(cat "$STDERR_FILE")"
|
|
109
|
+
else
|
|
110
|
+
pass "Test 2: https:// → silent passthrough"
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
# ---- Test 3: free port under :8080 contention -------------------------------
|
|
114
|
+
|
|
115
|
+
python3 -c "
|
|
116
|
+
import socket, time, sys
|
|
117
|
+
s = socket.socket()
|
|
118
|
+
try:
|
|
119
|
+
s.bind(('127.0.0.1', 8080))
|
|
120
|
+
s.listen(1)
|
|
121
|
+
print('READY', flush=True)
|
|
122
|
+
time.sleep(15)
|
|
123
|
+
except OSError as e:
|
|
124
|
+
print('SKIP', e, flush=True)
|
|
125
|
+
sys.exit(0)
|
|
126
|
+
" >/tmp/playwright-file-guard-test-blocker.stdout 2>&1 &
|
|
127
|
+
BLOCKER_PID=$!
|
|
128
|
+
TRACKED_PIDS+=("$BLOCKER_PID")
|
|
129
|
+
TMPFILES+=("/tmp/playwright-file-guard-test-blocker.stdout")
|
|
130
|
+
|
|
131
|
+
for _ in 1 2 3 4 5 6 7 8 9 10; do
|
|
132
|
+
if grep -qE 'READY|SKIP' /tmp/playwright-file-guard-test-blocker.stdout 2>/dev/null; then break; fi
|
|
133
|
+
sleep 0.1
|
|
134
|
+
done
|
|
135
|
+
|
|
136
|
+
if grep -q SKIP /tmp/playwright-file-guard-test-blocker.stdout 2>/dev/null; then
|
|
137
|
+
echo "SKIP: Test 3 — port 8080 already held by another process; cannot run contention test"
|
|
138
|
+
else
|
|
139
|
+
TEST_HTML2=$(mktemp -t playwright-file-guard-XXXXXX).html
|
|
140
|
+
echo "<html>2</html>" > "$TEST_HTML2"
|
|
141
|
+
TMPFILES+=("$TEST_HTML2")
|
|
142
|
+
INPUT_JSON=$(printf '{"hook_event_name":"PreToolUse","tool_name":"mcp__plugin_playwright_playwright__browser_navigate","tool_input":{"url":"file://%s"}}' "$TEST_HTML2")
|
|
143
|
+
STDOUT_FILE=$(mktemp); TMPFILES+=("$STDOUT_FILE")
|
|
144
|
+
printf '%s' "$INPUT_JSON" | bash "$HOOK" >"$STDOUT_FILE" 2>/dev/null
|
|
145
|
+
CHOSEN_PORT=$(grep -oE 'http://127\.0\.0\.1:[0-9]+/' "$STDOUT_FILE" | head -1 | grep -oE ':[0-9]+/' | tr -d ':/')
|
|
146
|
+
if [[ -z "$CHOSEN_PORT" ]]; then
|
|
147
|
+
fail "Test 3: hook did not produce a rewrite under contention. Stdout: $(cat "$STDOUT_FILE")"
|
|
148
|
+
elif [[ "$CHOSEN_PORT" == "8080" ]]; then
|
|
149
|
+
fail "Test 3: hook picked 8080 even though it was bound"
|
|
150
|
+
else
|
|
151
|
+
SERVER_PID=$(cat "/tmp/playwright-file-guard.${CHOSEN_PORT}.pid" 2>/dev/null || echo "")
|
|
152
|
+
[[ -n "$SERVER_PID" ]] && TRACKED_PIDS+=("$SERVER_PID")
|
|
153
|
+
pass "Test 3: free-port under :8080 contention picked $CHOSEN_PORT"
|
|
154
|
+
fi
|
|
155
|
+
fi
|
|
156
|
+
|
|
157
|
+
# ---- Test 4: stale-PID cleanup mode (explicit argv) -------------------------
|
|
158
|
+
|
|
159
|
+
STALE_DIR=$(mktemp -d); TMPFILES+=("$STALE_DIR")
|
|
160
|
+
( cd "$STALE_DIR" && exec python3 -u -m http.server 0 >/tmp/playwright-file-guard-stale.stdout 2>&1 ) &
|
|
161
|
+
STALE_PID=$!
|
|
162
|
+
TRACKED_PIDS+=("$STALE_PID")
|
|
163
|
+
TMPFILES+=("/tmp/playwright-file-guard-stale.stdout")
|
|
164
|
+
# Wait up to 2s for the http.server banner to flush.
|
|
165
|
+
for _ in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20; do
|
|
166
|
+
if grep -qE 'port [0-9]+' /tmp/playwright-file-guard-stale.stdout 2>/dev/null; then break; fi
|
|
167
|
+
sleep 0.1
|
|
168
|
+
done
|
|
169
|
+
STALE_PORT=$(grep -oE 'port [0-9]+' /tmp/playwright-file-guard-stale.stdout 2>/dev/null | head -1 | grep -oE '[0-9]+' || echo "")
|
|
170
|
+
if [[ -z "$STALE_PORT" ]]; then
|
|
171
|
+
fail "Test 4 setup: could not extract port from python3 http.server"
|
|
172
|
+
else
|
|
173
|
+
STALE_PID_FILE="/tmp/playwright-file-guard.${STALE_PORT}.pid"
|
|
174
|
+
echo "$STALE_PID" > "$STALE_PID_FILE"
|
|
175
|
+
TMPFILES+=("$STALE_PID_FILE")
|
|
176
|
+
touch -t "$(date -u -v-2H +%Y%m%d%H%M.%S 2>/dev/null || date -u -d '2 hours ago' +%Y%m%d%H%M.%S)" "$STALE_PID_FILE"
|
|
177
|
+
|
|
178
|
+
bash "$HOOK" cleanup >/dev/null 2>&1
|
|
179
|
+
|
|
180
|
+
# SIGTERM is asynchronous — wait up to 1s for the process to actually die.
|
|
181
|
+
for _ in 1 2 3 4 5 6 7 8 9 10; do
|
|
182
|
+
if ! kill -0 "$STALE_PID" 2>/dev/null; then break; fi
|
|
183
|
+
sleep 0.1
|
|
184
|
+
done
|
|
185
|
+
|
|
186
|
+
if kill -0 "$STALE_PID" 2>/dev/null; then
|
|
187
|
+
fail "Test 4: stale PID $STALE_PID still alive after cleanup"
|
|
188
|
+
kill -9 "$STALE_PID" 2>/dev/null || true
|
|
189
|
+
elif [[ -f "$STALE_PID_FILE" ]]; then
|
|
190
|
+
fail "Test 4: stale PID file $STALE_PID_FILE not removed after cleanup"
|
|
191
|
+
else
|
|
192
|
+
pass "Test 4: stale-PID cleanup killed http.server + removed PID file"
|
|
193
|
+
fi
|
|
194
|
+
fi
|
|
195
|
+
|
|
196
|
+
# ---- Test 5: reused-PID guard ----------------------------------------------
|
|
197
|
+
|
|
198
|
+
REUSED_PID_FILE="/tmp/playwright-file-guard.59999.pid"
|
|
199
|
+
echo "$$" > "$REUSED_PID_FILE"
|
|
200
|
+
TMPFILES+=("$REUSED_PID_FILE")
|
|
201
|
+
touch -t "$(date -u -v-2H +%Y%m%d%H%M.%S 2>/dev/null || date -u -d '2 hours ago' +%Y%m%d%H%M.%S)" "$REUSED_PID_FILE"
|
|
202
|
+
|
|
203
|
+
bash "$HOOK" cleanup >/dev/null 2>&1
|
|
204
|
+
|
|
205
|
+
if ! kill -0 "$$" 2>/dev/null; then
|
|
206
|
+
fail "Test 5: reused-PID guard killed our own bash process — fatal"
|
|
207
|
+
elif [[ -f "$REUSED_PID_FILE" ]]; then
|
|
208
|
+
fail "Test 5: reused-PID file $REUSED_PID_FILE not removed (hook should drop the stale ref)"
|
|
209
|
+
else
|
|
210
|
+
pass "Test 5: reused-PID guard left process alive, dropped stale PID file"
|
|
211
|
+
fi
|
|
212
|
+
|
|
213
|
+
# ---- Test 6: missing python3 fails open ------------------------------------
|
|
214
|
+
|
|
215
|
+
EMPTY_PATH_DIR=$(mktemp -d); TMPFILES+=("$EMPTY_PATH_DIR")
|
|
216
|
+
ln -s "$(command -v bash)" "$EMPTY_PATH_DIR/bash"
|
|
217
|
+
ln -s "$(command -v grep)" "$EMPTY_PATH_DIR/grep"
|
|
218
|
+
ln -s "$(command -v cat)" "$EMPTY_PATH_DIR/cat"
|
|
219
|
+
ln -s "$(command -v rm)" "$EMPTY_PATH_DIR/rm"
|
|
220
|
+
ln -s "$(command -v mktemp)" "$EMPTY_PATH_DIR/mktemp"
|
|
221
|
+
|
|
222
|
+
INPUT_JSON='{"hook_event_name":"PreToolUse","tool_name":"mcp__plugin_playwright_playwright__browser_navigate","tool_input":{"url":"file:///tmp/missing-python.html"}}'
|
|
223
|
+
STDOUT_FILE=$(mktemp); STDERR_FILE=$(mktemp); TMPFILES+=("$STDOUT_FILE" "$STDERR_FILE")
|
|
224
|
+
PATH="$EMPTY_PATH_DIR" printf '%s' "$INPUT_JSON" | PATH="$EMPTY_PATH_DIR" bash "$HOOK" >"$STDOUT_FILE" 2>"$STDERR_FILE"
|
|
225
|
+
RC=$?
|
|
226
|
+
|
|
227
|
+
if [[ "$RC" -ne 0 ]]; then
|
|
228
|
+
fail "Test 6: missing python3 should fail open (exit 0), got $RC"
|
|
229
|
+
elif [[ -s "$STDOUT_FILE" ]]; then
|
|
230
|
+
fail "Test 6: stdout should be empty on fail-open. Got: $(cat "$STDOUT_FILE")"
|
|
231
|
+
elif ! grep -q '\[playwright-file-guard\] action=fail reason=python3-missing' "$STDERR_FILE"; then
|
|
232
|
+
fail "Test 6: stderr missing python3-missing log line. Got: $(cat "$STDERR_FILE")"
|
|
233
|
+
else
|
|
234
|
+
pass "Test 6: missing python3 → fail open with diagnostic"
|
|
235
|
+
fi
|
|
236
|
+
|
|
237
|
+
# ---- Test 7: malformed stdin → silent passthrough ---------------------------
|
|
238
|
+
|
|
239
|
+
STDOUT_FILE=$(mktemp); STDERR_FILE=$(mktemp); TMPFILES+=("$STDOUT_FILE" "$STDERR_FILE")
|
|
240
|
+
printf '%s' 'not json at all { ' | bash "$HOOK" >"$STDOUT_FILE" 2>"$STDERR_FILE"
|
|
241
|
+
RC=$?
|
|
242
|
+
|
|
243
|
+
if [[ "$RC" -ne 0 ]]; then
|
|
244
|
+
fail "Test 7: malformed stdin should fail open (exit 0), got $RC"
|
|
245
|
+
elif [[ -s "$STDOUT_FILE" ]]; then
|
|
246
|
+
fail "Test 7: stdout should be empty on malformed stdin"
|
|
247
|
+
else
|
|
248
|
+
pass "Test 7: malformed stdin → silent passthrough"
|
|
249
|
+
fi
|
|
250
|
+
|
|
251
|
+
# ---- Test 8: terminal stdin guard preserved --------------------------------
|
|
252
|
+
|
|
253
|
+
if ! grep -q '\[ -t 0 \]' "$HOOK"; then
|
|
254
|
+
fail "Test 8: terminal stdin guard ([ -t 0 ]) missing from hook"
|
|
255
|
+
else
|
|
256
|
+
pass "Test 8: terminal stdin guard present"
|
|
257
|
+
fi
|
|
258
|
+
|
|
259
|
+
# ---- Test 9: zero-grep-hits for old menu copy in this hook ------------------
|
|
260
|
+
|
|
261
|
+
# The pattern uses [l] to break self-match: this file's bytes contain "[l]ocal"
|
|
262
|
+
# but the hook source contains "local" if the menu copy was reintroduced. The
|
|
263
|
+
# extended-regex bracket class matches "local" without our query containing
|
|
264
|
+
# the literal string we're forbidding.
|
|
265
|
+
OLD_MENU_PATTERN='Start a [l]ocal HTTP server'
|
|
266
|
+
if grep -qE "$OLD_MENU_PATTERN" "$HOOK"; then
|
|
267
|
+
fail "Test 9: hook still contains the old menu copy"
|
|
268
|
+
else
|
|
269
|
+
pass "Test 9: hook does not contain the old menu copy"
|
|
270
|
+
fi
|
|
271
|
+
|
|
272
|
+
echo
|
|
273
|
+
echo "──────── playwright-file-guard test summary ────────"
|
|
274
|
+
echo "PASS: $PASS"
|
|
275
|
+
echo "FAIL: $FAIL"
|
|
276
|
+
|
|
277
|
+
[[ "$FAIL" -gt 0 ]] && exit 1
|
|
278
|
+
exit 0
|